package se.lnu.axez;

import java.util.List;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import se.lnu.axez.physics.bullet.PhysicsManager;
import se.lnu.axez.shaders.ShaderManager;
import se.lnu.axez.shaders.ShaderManager.ShaderFactory;
import se.lnu.axez.textures.TextureManager;
import se.lnu.axez.textures.TextureManager.TextureFactory;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.view.MotionEvent;

/**
 * {@link Renderer} implementation which supports axeZ architecture.
 * @author stan
 *
 */
final class AxezRenderer implements GLSurfaceView.Renderer {
	private final List<IControl> controls;
	private final TriMeshManager triMeshManager;
	private final ShaderManager shaderManager;
	private final RenderMatrices renderMatrices;
	private final TextureManager textureManager;
	private final PhysicsManager physicsManager;
	private final AxezContext drawingContext;
	private IObjectPickListener onObjectPickedListener;
	private Camera activeCamera;
	private int viewportWidth;
	private int viewportHeight;
	private MotionEvent motionEvent;

	/**
	 * Instantiates new {@link AxezRenderer}.
	 * 
	 * @param controls
	 *            - list of controls to be rendered on the scene. Can be empty
	 *            but cannot be null.
	 * @param shaderManager
	 *            - {@link ShaderManager} used in current axeZ view. Cannot be
	 *            null.
	 * @param triMeshManager
	 *            - {@link TriMeshManager} used in current axeZ view. Cannot be
	 *            null.
	 * @param camera
	 *            - {@link Camera} used to render the scene. Cannot be null.
	 */
	public AxezRenderer(List<IControl> controls, Context context, Camera camera) {
		if (controls == null) {
			throw new IllegalArgumentException("controls parameter cannot be null.");
		}
		
		if (camera == null) {
			throw new IllegalArgumentException("camera parameter cannot be null.");
		}
		
		this.controls = controls;
		this.shaderManager = new ShaderManager(new ShaderFactory(context.getResources()));
		this.renderMatrices = new RenderMatrices();
		this.textureManager = new TextureManager(new TextureFactory(context.getResources()));
		this.triMeshManager = new TriMeshManager(context);
		this.physicsManager = new PhysicsManager();
		this.drawingContext = new AxezContext(
				context,
				this.renderMatrices, 
				this.triMeshManager, 
				this.shaderManager, 
				this.textureManager, 
				this.physicsManager);
		
		this.setActiveCamera(camera);
	}

	/**
	 * Sets active camera for scene rendering.
	 * 
	 * @param camera
	 *            - camera to be used for scene rendering. Cannot be null.
	 */
	public void setActiveCamera(Camera camera) {
		if (camera == null) {
			throw new IllegalArgumentException("camera parameter cannot be null.");
		}

		this.activeCamera = camera;
		this.activeCamera.initializeViewport(0, 0, this.viewportWidth, this.viewportHeight);
		this.activeCamera.setProjectionMatrix(this.renderMatrices);
	}

	public Camera getActiveCamera() {
		return this.activeCamera;
	}

	void setOnPickListener(IObjectPickListener listener) {
		this.onObjectPickedListener = listener;
	}
	
	synchronized void onTouchEvent(MotionEvent motionEvent) {
		this.activeCamera.getCameraCoordinates(motionEvent.getX(), motionEvent.getY(), this.drawingContext.values, AxezContext.CAMERA_PICK_COORDINATES_OFFSET);
		this.drawingContext.needRayTest = true;
		this.motionEvent = motionEvent;
	}
	
	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		GLES20.glClearColor(0f, 0f, 0f, 1.0f);
		GLES20.glEnable(GLES20.GL_CULL_FACE);
		GLES20.glCullFace(GLES20.GL_BACK);
		GLES20.glEnable(GLES20.GL_DEPTH_TEST);
		GLES20.glDepthFunc(GLES20.GL_LEQUAL);

		this.initControls();
		this.loadShaders();
		this.loadTextures();
		this.loadPhysics();
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int w, int h) {
		this.viewportWidth = w;
		this.viewportHeight = h;
		GLES20.glViewport(0, 0, this.viewportWidth, this.viewportHeight);
		this.activeCamera.initializeViewport(0, 0, this.viewportWidth, this.viewportHeight);
		this.activeCamera.setProjectionMatrix(this.renderMatrices);
	}

	@Override
	public synchronized void onDrawFrame(GL10 gl) {
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
		this.renderMatrices.reset();
		
		this.drawingContext.pickDistance = Float.MAX_VALUE;
		this.drawingContext.pickedObject = null;

		this.activeCamera.setModelViewMatrix(this.renderMatrices);

		if (this.controls != null) {
			for (IControl control : this.controls) {
				control.draw(this.drawingContext);
			}
		}
		
		this.drawingContext.needRayTest = false;
		if (this.drawingContext.pickedObject != null) {
			((IControl)this.drawingContext.pickedObject).onTouchEvent(this.motionEvent);
			IObjectPickListener listener = this.onObjectPickedListener;
			if (listener != null) {
				listener.onObjectPicked(this.drawingContext.pickedObject);
			}
		}
		
		this.motionEvent = null;
	}

	/**
	 * Initializes all renderable visuals registered in the scene. This method
	 * must be called within OpenGL context.
	 */
	private void initControls() {
		for (IControl control : this.controls) {
			control.init(this.drawingContext);
		}
	}
	
	// TODO: add onSurfaceDestroyed method to release GL buffers

	/**
	 * Loads shaders within OpenGL context.
	 */
	private void loadShaders() {
		this.shaderManager.loadShaders();
	}
	
	private void loadTextures() {
		this.textureManager.loadTextures();
	}

	private void loadPhysics() {
		this.physicsManager.createAll().startWorldSimulations();
	}
}
